#include <slothttpd/server.hpp>
#include <slothttpd/logger.hpp>

#include <cerrno>
#include <cstring>

#include <string>
#include <stdexcept>
#include <iostream>

#include <sys/prctl.h>
#include <unistd.h>
#include <sys/wait.h>

#include <boost/bind.hpp>
#include <boost/exception/diagnostic_information.hpp>
#include <boost/lexical_cast.hpp>
#include <boost/make_shared.hpp>

namespace slothttpd {

worker::worker() :
    pid(-1)
{
}

void worker::stop()
{
    send_signal(SIGTERM);
}

void worker::kill()
{
    send_signal(SIGKILL);
}

void worker::send_signal(int signo)
{
    if (pid != -1) {
        if (::kill(pid, signo) != 0) {
            logger() << "can't send signal " << signo << " to " << pid << std::endl;
        }
    }
}

boost::scoped_ptr<server> server::instance;
config::configuration_ptr server::conf;

server::server(config::configuration_ptr cfg) :
    is_master_      (true),
    is_stopping_    (false),
    acceptor_       (io_service),
    signals_        (io_service)
{
    conf = cfg;
    
    set_process_title("slothttpd-master");
    
    signals_.add(SIGTERM);
    signals_.add(SIGINT);
    signals_.add(SIGQUIT);
    signals_.add(SIGCHLD);
    signals_.async_wait(boost::bind(
        &server::handle_signal, 
        this,
        boost::asio::placeholders::signal_number));
    
    struct sigaction ignore_pipe_action;
    
    std::memset(&ignore_pipe_action, 0, sizeof ignore_pipe_action);
    ignore_pipe_action.sa_handler = SIG_IGN;
    
    if (::sigaction(SIGPIPE, &ignore_pipe_action, nullptr) != 0) {
        logger() << "can't ignore SIGPIPE: " << strerror(errno) << std::endl;
        BOOST_THROW_EXCEPTION(std::runtime_error("can't ignore sigpipe"));
    }
    
    boost::asio::ip::tcp::resolver          resolver(io_service);
    boost::asio::ip::tcp::resolver::query   query(conf->bound_address, 
                                                  boost::lexical_cast<std::string>(conf->port));
    boost::asio::ip::tcp::endpoint          endpoint = *resolver.resolve(query);
    
    acceptor_.open(endpoint.protocol());
    acceptor_.set_option(boost::asio::ip::tcp::acceptor::reuse_address(true));
    acceptor_.bind(endpoint);
    acceptor_.listen();
    
    if (server::conf->workers == 0) {
        worker_cycle();
    } else {
        for (unsigned i = 0; i < server::conf->workers; i++) {
            spawn_worker();
            
            if (!is_master_)
                break;
        }
        
        if (is_master_) {
            master_cycle();
        } else {
            worker_cycle();
        }
    }
}

boost::asio::io_service &server::get_service()
{
    return io_service;
}

void server::set_process_title(const char *name)
{
    if (prctl(PR_SET_NAME, reinterpret_cast<unsigned long>(name)) != 0) {
        logger() << "can't set process title" << std::endl;
    }
}

void server::spawn_worker()
{
    worker *w = new worker();
    
    try {
        io_service.notify_fork(boost::asio::io_service::fork_prepare);
        
        pid_t pid = ::fork();
        
        switch (pid) {
        case 0:
            io_service.notify_fork(boost::asio::io_service::fork_child);
            server::set_process_title("slothttpd-work");
            is_master_ = false;
            break;
            
        case -1:
            logger() << "can't spawn worker: " << std::strerror(errno) << std::endl;
            io_service.notify_fork(boost::asio::io_service::fork_parent);
            delete w;
            break;
            
        default:
            w->pid = pid;
            workers_.push_front(*w);
            io_service.notify_fork(boost::asio::io_service::fork_parent);
            break;
        }
    } catch (...) {
        delete w;
        throw;
    }
}

void server::master_cycle()
{
}

void server::worker_cycle()
{
    aio = boost::make_shared<aio::linux_aio_context>(io_service);
    
    post_accept();
}

void server::post_accept()
{
    new_connection_ = new connection(io_service);
    
    acceptor_.async_accept(
        new_connection_->socket,
        boost::bind(&server::handle_accept,
                    this,
                    boost::asio::placeholders::error));
}

void server::handle_accept(const boost::system::error_code& e)
{
    if (!e) {
        try {
            new_connection_->start();
        } catch (const std::exception &ex) {
            logger() << ex.what() << std::endl;
        }
        
        new_connection_ = nullptr;
    
        // TODO: use timers to retry this on fail
        post_accept();
    } else {
        new_connection_ = nullptr;
        stop();
    }
    //post_accept();
}

void server::handle_signal(int signo)
{
    if (signo == SIGCHLD) {
        if (is_master_) {
            pid_t pid;
            
            while ((pid = ::waitpid(-1, NULL, WNOHANG)) > 0) {
                
                for (auto it = workers_.begin(); it != workers_.end(); ++it) {
                    if (it->pid == pid) {
                        workers_.erase_and_dispose(it, [=](worker *w) { delete w; });
                        break;
                    }
                }
            }
            
            if (!is_stopping_) {
                auto to_spawn = server::conf->workers - workers_.size();
                
                if (to_spawn > 0) {
                    for (unsigned i = 0; i < to_spawn; i++) {
                        spawn_worker();
                    }
                }
            }
        }
    } else {
        logger() << "got stop signal: " << signo << std::endl;
        
        // TODO: wait for workers to exit, kill after timeout
        for (auto &w : workers_) {
            w.stop();
        }
        
        stop();
    }
}

void server::run()
{
    io_service.run();
}

void server::stop()
{
    acceptor_.close();
    signals_.cancel();
    
    connection::close_all();
        
    for (auto &app : conf->fcgi_apps) {
        app->close();
    }
    
    if (!is_stopping_) {
        logger() << "max connections: " << connection::max_connections << '\n';
        logger() << "total requests: " << connection::total_requests << std::endl;
    }
    
    is_stopping_ = true;
}

}
